/*
 * Copyright 2009 Tran Nhut Trung.
 * 
 * Licensed under the Apache License, Version 2.0 (the "License"); 
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, 
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and 
 * limitations under the License.
 */
package com.gdevelop.gwtxp.rebind;


import com.gdevelop.gwtxp.rebind.el.ELParser;
import com.gdevelop.gwtxp.rebind.el.ELParserContext;

import com.gdevelop.gwtxp.rebind.el.Node;

import com.google.gwt.core.ext.GeneratorContext;
import com.google.gwt.core.ext.typeinfo.JClassType;
import com.google.gwt.core.ext.typeinfo.JType;
import com.google.gwt.core.ext.typeinfo.NotFoundException;
import com.google.gwt.i18n.client.DateTimeFormat;
import com.google.gwt.user.client.ui.AbstractImagePrototype;
import com.google.gwt.user.client.ui.FocusWidget;
import com.google.gwt.user.client.ui.HasCaption;
import com.google.gwt.user.client.ui.HasHTML;
import com.google.gwt.user.client.ui.HasText;
import com.google.gwt.user.client.ui.HasValue;
import com.google.gwt.user.client.ui.HasWordWrap;
import com.google.gwt.user.rebind.SourceWriter;

import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.List;

import java.util.StringTokenizer;

import org.eclipse.core.databinding.observable.list.IObservableList;

import org.w3c.dom.Attr;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;


public abstract class AbstractTag implements Tag{
  protected Tag parent;
  protected List<Tag> children = new ArrayList<Tag>();
  
  protected String varName;
  protected Element elem;
  protected SourceWriter out;
  protected GeneratorContext context;
  
  private boolean generateInitDone = false;
  protected ELParserContext elContext;
  
  /**
   * Common used types
   */
  protected JClassType TYPE_AbstractImagePrototype;
  protected JClassType TYPE_IObservableList;
  protected JClassType TYPE_List;
  protected JClassType TYPE_String;
  protected JClassType TYPE_Integer;
  protected JClassType TYPE_Date;

  public AbstractTag() {
  }

  public void add(Tag tag) {
    children.add(tag);
    if (tag instanceof AbstractTag){
      ((AbstractTag)tag).parent = this;
    }
  }
  public Tag getParent() {
    return parent;
  }
  public List<Tag> getChildren() {
    return Collections.unmodifiableList(children);
  }


  public void setVarName(String varName) {
    this.varName = varName;
  }
  public String getVarName() {
    return varName;
  }

  public void setElement(Element elem) {
    this.elem = elem;
  }
  public Element getElement() {
    return elem;
  }

  public void setSourceWriter(SourceWriter out) {
    this.out = out;
  }
  public SourceWriter getSourceWriter() {
    return out;
  }

  public void setELParserContext(ELParserContext ctx) throws NotFoundException{
    this.elContext = ctx;
    this.context = elContext.getGeneratorContext();

    TYPE_AbstractImagePrototype = context.getTypeOracle().getType(AbstractImagePrototype.class.getName());
    TYPE_IObservableList = context.getTypeOracle().getType(IObservableList.class.getName());
    TYPE_List = context.getTypeOracle().getType(List.class.getName());
    TYPE_String = context.getTypeOracle().getType(String.class.getName());
    TYPE_Integer = context.getTypeOracle().getType(Integer.class.getName());
    TYPE_Date = context.getTypeOracle().getType(Date.class.getName());
  }
  public ELParserContext getELParserContext() {
    return elContext;
  }

  public String[] getImports() {
    return new String[0];
  }

  public void generateDeclaration() throws NotFoundException {
    String className = getUIClass().getSimpleName();
    out.println(className + " " + varName + " = new " + className+ "();");
  }
  
  public void generateInitStart() throws NotFoundException{
    if (generateInitDone){
      return;
    }
    
    out.println();
    out.println("// " + varName);
    if (parent == null){
      out.println(GwtXPCreator.VAR_NAME_ROOT_UI + " = " + varName + ";");
    }
    if (getUIClass() != null){
      out.println(GwtXPCreator.VAR_NAME_UI_MAP +  ".put(\"" + varName + "\", " + varName + ");");
    }

    if (getUIClass() == null){
      return;
    }

    NamedNodeMap attrs = elem.getAttributes();
    for (int i=0; i<attrs.getLength(); i++){
      Attr attr = (Attr)attrs.item(i);
      if (("j_name".equals(attr.getLocalName())) 
        || ("validator".equals(attr.getLocalName()))
        || ("pattern".equals(attr.getLocalName()))
      ){
        continue;
      }
      //? if (attr.isDeferredMethodInput()){
      if (isDeferredMethodInput(attr)){
        // move out of this for block.
        // generateMethodBinding(attr);
        continue;
      }
      
      String attrValue = attr.getValue();
      if ((attrValue == null) || (attrValue.length() == 0)){
        continue;
      }
      
      Node node = ELParser.parse(attr.getValue());
      JType returnType = node.getReturnType(elContext);
      boolean isCollectionType = false;
      if (returnType.isArray() != null){
        isCollectionType = true;
      }
      if (returnType instanceof JClassType){
        if (TYPE_List.isAssignableFrom((JClassType)returnType)){
          isCollectionType = true;
        }
      }
      
      if (node.isStaticValue(elContext) && !isCollectionType){
        generateSetAttributeByExpression(attr);
      }else if (isCollectionType){
        generateListBinding(attr);
      }else{
        generateValueBinding(attr);
      }
    }

    // generate MethodBinding after setup bindings
    for (int i=0; i<attrs.getLength(); i++){
      Attr attr = (Attr)attrs.item(i);
      //? if (attr.isDeferredMethodInput()){
      if (isDeferredMethodInput(attr)){
        generateMethodBinding(attr);
      }
    }
  }
  public void generateInitEnd() throws NotFoundException{
    generateInitDone = true;
  }

  public String getAttribute(String attrName){
    String attrValue = elem.getAttribute(attrName);
    if (attrValue != null){
      attrValue = attrValue.trim();
    }
    if (attrValue.length() == 0){
      return null;
    }
    return attrValue;
  }

  protected void generateSetAttributeByExpression(Attr attr) throws NotFoundException {
    String attrName = attr.getLocalName();

    // Additional style names and style dependent names
    if (("addStyleNames".equals(attrName)) || ("addStyleDependentNames".equals(attrName))){
      StringTokenizer st = new StringTokenizer(attr.getValue(), " ,;");
      while (st.hasMoreTokens()){
        String styleName = st.nextToken();
        Node node = ELParser.parse(styleName);
        if ("addStyleNames".equals(attr.getLocalName())){
          out.print(varName + ".addStyleName(");
        }else{
          out.print(varName + ".addStyleDependentName(");
        }
        out.print(node.generateGetExpressionWithConversion(elContext, TYPE_String));
        out.println(");");
      }
    }else{
      Node node = ELParser.parse(attr.getValue());
      JClassType toType = elContext.getPropertyType(context.getTypeOracle().getType(getUIClass().getName()), attrName);
      out.print(varName + "." + elContext.getSetMethodName(attrName) + "(");
      out.print(node.generateGetExpressionWithConversion(elContext, toType));
      out.println(");");
    }
  }

  protected void generateListBinding(Attr attr) throws NotFoundException {
    Node node = ELParser.parse(attr.getValue());
    node = node.jjtGetChild(0);

    out.println(GwtXPCreator.VAR_NAME_DATABINDING_CONTEXT + ".bindList(");
    out.indent();
    generateTargetObservaleList(attr);
    out.print(", ");
    generateModelObservaleList(attr);
    out.print(", ");
    generateTargetToModelUpdateListStrategy(attr);
    out.print(", ");
    generateModelToTargetUpdateListStrategy(attr);
    out.outdent();
    out.println(");");
  }
  
  protected void generateValueBinding(Attr attr) throws NotFoundException {
    Node node = ELParser.parse(attr.getValue());
    node = node.jjtGetChild(0);

    out.println(GwtXPCreator.VAR_NAME_DATABINDING_CONTEXT + ".bindValue(");
    out.indent();
    generateTargetObservaleVaue(attr);
    out.print(", ");
    generateModelObservaleValue(attr);
    out.print(", ");
    generateTargetToModelUpdateValueStrategy(attr);
    out.print(", ");
    generateModelToTargetUpdateValueStrategy(attr);
    out.outdent();
    out.println(");");
  }
  protected void generateMethodBinding(Attr attr) throws NotFoundException {
    Node node = ELParser.parse(attr.getValue());
    node = node.jjtGetChild(0);

    String eventName = attr.getLocalName();
    eventName = eventName.substring(2);
    out.println(varName + ".add" + eventName + "Handler(new " + eventName + "Handler(){");
    out.indent();
    out.println("public void on" + eventName + "(" + eventName + "Event event){");
    out.indent();
    if (attr.getValue().startsWith("#{#")){
      String token = attr.getValue().substring(3, attr.getValue().length()-1);
      out.println("History.newItem(\"" + token + "\", true);");
    }else{
      out.println(node.jjtGetParent().generateMethodBinding(elContext));
    }
    out.outdent();
    out.println("}");
    out.outdent();
    out.println("});");
  }
  
  /**
   * Sub-class have to overwrite this method for actual UI ObservaleList generation
   * @param attr
   */
  protected void generateTargetObservaleList(Attr attr) throws NotFoundException {
    out.println("new WritableList()");
  }

  protected void generateModelObservaleList(Attr attr) throws NotFoundException {
    Node node = ELParser.parse(attr.getValue());
    JClassType returnType = node.getReturnType(elContext);
    if (TYPE_IObservableList.isAssignableFrom(returnType)){
      // Bind the return IObservableList directly.
      out.print(node.generateGetExpression(elContext));
    }else{
      JType elementType = null;
      out.print("new WritableList(");
      if (returnType.isArray() != null){
        out.print("Arrays.asList(");
        elementType = returnType.isArray().getComponentType();
      }else{
        //TODO: elementType = context.getTypeOracle().getType((attr.getExpectedTypeName()));
      }
      out.print(node.generateGetExpression(elContext));
      if (returnType.isArray() != null){
        out.print(")");
      }
      out.println(", " + elementType.getQualifiedSourceName() + ".class" + ")");
    }
  }

  protected void generateTargetToModelUpdateListStrategy(Attr attr){
    out.println("null");
  }
  protected void generateModelToTargetUpdateListStrategy(Attr attr){
    out.println("null");
  }

  protected void generateTargetObservaleVaue(Attr attr){
    boolean hasText = false;
    Class uiClass = this.getUIClass();
    if ((uiClass != null) && (HasText.class.isAssignableFrom(uiClass))){
      hasText = true;
    }
    boolean hasHTML = false;
    if ((uiClass != null) && (HasHTML.class.isAssignableFrom(uiClass))){
      hasHTML = true;
    }
    boolean hasCaption = false;
    if ((uiClass != null) && (HasCaption.class.isAssignableFrom(uiClass))){
      hasCaption = true;
    }
    boolean hasValue = false;
    if ((uiClass != null) && (HasValue.class.isAssignableFrom(uiClass))){
      hasValue = true;
    }
    boolean hasWordWrap = false;
    if ((uiClass != null) && (HasWordWrap.class.isAssignableFrom(uiClass))){
      hasWordWrap = true;
    }
    boolean isFocusWidget = false;
    if ((uiClass != null) && (FocusWidget.class.isAssignableFrom(uiClass))){
      isFocusWidget = true;
    }
    
    if ((hasHTML) && ("HTML".equals(attr.getLocalName()))){
      out.println("new HasHTMLObservableValue(" + varName + ")");
    }else if ((hasText) && ("text".equals(attr.getLocalName()))){
      out.println("new HasTextObservableValue(" + varName + ")");
    }else if ((hasCaption) && ("caption".equals(attr.getLocalName()))){
      out.println("new HasCaptionObservableValue(" + varName + ")");
    }else if ((hasValue) && ("value".equals(attr.getLocalName()))){
      out.print("new HasValueObservableValue(" + varName + ", ");
      Type[] types = uiClass.getGenericInterfaces();
      for (Type type : types){
        if (type instanceof ParameterizedType){
          ParameterizedType parameterizedType = (ParameterizedType)type;
          if (parameterizedType.getRawType() == HasValue.class){
            Class actualClass = (Class)parameterizedType.getActualTypeArguments()[0];
            out.print(actualClass.getName() + ".class");
            break;
          }
        }
      }
      out.println(")");
    }else if ((hasWordWrap) && ("wordWrap".equals(attr.getLocalName()))){
      out.println("new HasWordWrapObservableValue(" + varName + ")");
    }else if ((isFocusWidget) && ("enabled".equals(attr.getLocalName()))){
      out.println("new EnabledObservableValue(" + varName + ")");
    }else{
      out.println("new WidgetPropertyObservableValue(" + varName + ", \"" + attr.getLocalName() + "\")");
    }
  }
  
  protected void generateModelObservaleValue(Attr attr) throws NotFoundException {
    Node node = ELParser.parse(attr.getValue());
    out.println(node.generateModelObservaleValue(elContext));
  }

  /**
   * Generate target to model UpdateValueStrategy
   * Sub-class may overwrite to add validators, converter format pattern
   * This class uses 'null' for default UpdateValueStrategy
   */
  protected void generateTargetToModelUpdateValueStrategy(Attr attr) throws NotFoundException {
    // out.print("new GWTUpdateValueStrategy()");
    out.print("null");
    out.println();
  }


  /**
   * Generate model to target UpdateValueStrategy
   * Sub-class may overwrite to add validators, converter format pattern
   * This class uses 'null' for default UpdateValueStrategy
   */
  protected void generateModelToTargetUpdateValueStrategy(Attr attr) throws NotFoundException{
    // out.println("new GWTUpdateValueStrategy()");
    out.print("null");
    out.println();
  }
  

  protected String generateSetPatternedConverter(JClassType fromType, JClassType toType, String pattern){
    StringBuffer buffer = new StringBuffer();
    
    if (fromType == toType){
      return "";
    }
    
    if ((fromType == TYPE_String) && elContext.isNumberType(toType)){
      buffer.append(".setConverter(new StringToNumberConverter(" + 
        toType.getQualifiedSourceName() + ".class, NumberFormat.getFormat(\"" + pattern + "\")))");
    }
    if (elContext.isNumberType(fromType) && (toType == TYPE_String)){
      buffer.append(".setConverter(new NumberToStringConverter(" + 
        fromType.getQualifiedSourceName() + ".class, NumberFormat.getFormat(\"" + pattern + "\")))");
    }
    
    if ((fromType == TYPE_String) && elContext.isDateType(toType)){
      buffer.append(".setConverter(new StringToDateConverter(" + 
        "DateTimeFormat.getFormat(\"" + pattern + "\")))");
    }
    if (elContext.isDateType(fromType) && (toType == TYPE_String)){
      buffer.append(".setConverter(new DateToStringConverter(" + 
        "DateTimeFormat.getFormat(\"" + pattern + "\")))");
    }

    return buffer.toString();
  }
  
  // Simulate org.apache.jasper.compiler.Node.JspAttribute#isDeferredMethodInput
  // TODO: Any better ways?
  private boolean isDeferredMethodInput(Attr attr){
    String value = attr.getValue();
    return value.startsWith("#");
  }
}
